﻿
package extremefx.i18n {
	import extremefx.DateTime;
	import extremefx.i18n.Calendar;	

	/**
	 * @author Marcelo Volmaro
	 * <p>This is the Persian calendar of Iran, also known as the Iranian 
	 * calendar or the Jalaali calendar. The Afghan calendar may or may
	 * not be different, as different sources disagree about it.</p>
	 * <p>The implemented algorithm is the simple 33-year arithmetic
	 * calendar, which is not the same as the official Iranian calendar.
	 * But this arithmetic calendar has been confirmed to produce the
	 * same results as the official Iranian calendar at least
	 * from 1925 C.E., when the calendar was officially introduced,
	 * to 2088 C.E. This is the same algorithm that is used in .NET.</p>
	 * <p>The Iranian law explicitly mentions that the true solar year
	 * should be used, which requires astronomical calculations of the
	 * March equinox and the solar apparent noon. The exact locale for
	 * observation of the apparent noon is not mentioned in the 1925 law,
	 * but the current practice is using the 52.5 E meridian, which is
	 * the meridian defining the official timezone of Iran.</p>
	 * <p>Also, please note that implementing the Persian calendar
	 * using the 2820-year arithmetic algorithm, as suggested by
	 * Ahmad Birashk and others, is less accurate than the 33-year
	 * calendar: first, it fails earlier than the 33-year cycle in
	 * matching the official astronomical calendar (first failure is
	 * in 2025 C.E.), and second, the 2820-year suggested rule is based
	 * on the mean tropical year, not the mean March equinoctial year.</p>
	 */
	public class PersianCalendar extends Calendar {
		internal static var PERSIAN_ERA:uint = 1;
		private static const MIN:DateTime = new DateTime (622, 3, 21, 0, 0, 0);
		private static const MAX:DateTime = new DateTime (9999, 12, 31, 11, 59, 59);
		
		/**
		 * The DateTime ticks for first day of year 1 A.P.
		 */
		private static const MIN_TICKS:Number = 196036416000000;
		
		/**
		 * The minimum year in the A.P. era supported.
		 */
		private static const MIN_YEAR:int = 1;
		
		public function PersianCalendar() {
			super();
				
			_abbrEraNames = ["A.P."];
			_eraNames = ["Anno Persico"];
			if (_twoDigitYearMax == 99) _twoDigitYearMax = 1410;
		}
		
		private function _checkEra(pEra:Array):void {
			var era:uint = pEra[0];
			if (era == -1) era = PERSIAN_ERA;
			if (era != PERSIAN_ERA) throw new ArgumentError("Era value was not valid.");
			pEra[0] = era;
		}
		
		protected override function _checkYE(pYear:int, pEra:Array):void {
			_checkEra(pEra);
			if (pYear < MIN_YEAR || pYear > _getMaxYear()) throw new RangeError("pYear", "Only Persian years between 1 and 9378, inclusive, are supported.");
		}
		
		protected function _checkYME(pYear:int, pMonth:int, pEra:Array):void {
			_checkYE(pYear, pEra);
			if (pMonth < 1 || pMonth > 12){
				throw new RangeError("pMonth", "Month must be between one and twelve.");
				
			} else if (pYear == _getMaxYear() && pMonth > 10) {
				throw new RangeError("pMonth", "Months in year 9378 must be between one and ten.");
			}
		}
		
		protected function _checkYMDE(pYear:int, pMonth:int, pDay:int, pEra:Array):void {
			_checkYME(pYear, pMonth, pEra);
			_argumentInRange("pDay", pDay, 1, getDaysInMonth(pYear, pMonth, pEra[0]));
			if (pYear == _getMaxYear() && pMonth == 10 && pDay > 10) throw new RangeError("pDay", "Days in month 10 of year 9378 must be between one and ten.");
		}
		
		private function _checkDateTime(pTime:DateTime):void {
			if (pTime.ticks < MIN_TICKS) throw new RangeError("pTime", "Only positive Persian years are supported.");
		}
		
		public override function get eras():Array {
			return [PERSIAN_ERA];  
		}
		
		public override function set twoDigitYearMax(pYear:int):void{ 
			_argumentInRange ("value", pYear, MIN_YEAR, _getMaxYear());
			_twoDigitYearMax = pYear;
		}
		
		public override function getDayOfMonth(pTime:DateTime):int {
			_checkDateTime(pTime);
			var rd:int = CCFixed._fromDateTime(pTime);
			return int(CCPersian._dmyFromFixed(rd)[2]);
		}
		
		public override function getDayOfWeek(pTime:DateTime):int {
			_checkDateTime(pTime);
			var rd:int = CCFixed._fromDateTime(pTime);
			return CCFixed._dayOfWeek(rd);
		}
		
		public override function getDayOfYear(pTime:DateTime):int {
			_checkDateTime(pTime);
			var rd:int = CCFixed._fromDateTime(pTime);
			var year:int = CCPersian._yearFromFixed(rd);
			var rd1_1:int = CCPersian._fixedFromDMY(1, 1, year);
			return rd - rd1_1 + 1;
		}
		
		public override function getDaysInMonth(pYear:int, pMonth:int, pEra:int = -1):int{
			_checkYME(pYear, pMonth, [pEra]);
			if (pMonth <= 6) {
				return 31;
				
			} else if (pMonth == 12 && !isLeapYear(pYear)) {
				return 29;
				
			} else {
				return 30;
			}
		}
		
		public override function getDaysInYear(pYear:int, pEra:int = -1):int {
			_checkYE(pYear, [pEra]);
			return isLeapYear(pYear) ? 366 : 365;
		}
		
		public override function getEra(pTime:DateTime):int {
			_checkDateTime(pTime);
			return PERSIAN_ERA;
		}
		
		public override function getLeapMonth(pYear:int, pEra:int = -1):int {
			return 0;
		}
		
		public override function getMonth(pTime:DateTime):int {
			_checkDateTime(pTime);
			var rd:int = CCFixed._fromDateTime(pTime);
			return int(CCPersian._dmyFromFixed(rd)[1]);
		}
		
		public override function getMonthsInYear(pYear:int, pEra:int = -1):int {
			_checkYE(pYear, [pEra]);
			return 12;
		}
		
		public override function getYear(pTime:DateTime):int {
			_checkDateTime(pTime);
			return CCPersian._yearFromFixed(CCFixed._fromDateTime(pTime));
		}
		
		public override function isLeapDay(pYear:int, pMonth:int, pDay:int, pEra:int= -1):Boolean {
			_checkYMDE(pYear, pMonth, pDay, [pEra]);
			return isLeapYear(pYear) && pMonth == 12 && pDay == 30;
		}
		
		public override function isLeapMonth(pYear:int, pMonth:int, pEra:int= -1):Boolean {
			_checkYME(pYear, pMonth, [pEra]);
			return false;
		}
		
		public override function isLeapYear(pYear:int, pEra:int= -1):Boolean {
			_checkYE(pYear, [pEra]);
			return CCPersian._isLeapYear(pYear);
		}
		
		public override function toDateTime(pYear:int, pMonth:int, pDay:int, pHour:int, pMinute:int, pSecond:int, pMSecond:int, pEra:int = -1):DateTime {
			_checkYMDE(pYear, pMonth, pDay, [pEra]);
			_checkHMSM(pHour, pMinute, pSecond, pMSecond);
			var rd:int = CCPersian._fixedFromDMY(pDay, pMonth, pYear);
			return CCFixed._toDateTime2(rd, pHour, pMinute, pSecond, pMSecond);
		}
		
		public override function toFourDigitYear (pYear:int):int {
			_argumentInRange ("pYear", pYear, 0, 99);
			
			var baseExtra:int = _twoDigitYearMax % 100;
			var baseCentury:int = _twoDigitYearMax - baseExtra;
			
			if (pYear >= 100) return pYear;
			
			if (pYear <= baseExtra){
				return baseCentury + pYear;
				
			} else{
				return baseCentury + pYear - 100;
			}
		}
		
		public override function get algorithmType():uint{
			return CalendarAlgorithmType.SOLAR;
		}
		
		public override function get minSupportedDateTime():DateTime {
			return MIN;
		}
		
		public override function get maxSupportedDateTime():DateTime {
			return MAX;
		}
	}
}
